/**
 * Image Auto Upload script for Hexo static site generator.
 *
 * Automatically detects linked external images on each post, downloads them
 * into the corresponding folder for the post and puts new image urls in the
 * post body.
 *
 * IMPORTANT!!
 * To work properly set `post_asset_folder` to `true` in _config.yml
 * And install hexo-asset-link plugin:
 *   npm i -s hexo-asset-link
 *
 * To use:
 * - create a folder called `scripts` on Hexo root and put this inside.
 * - then run `hexo generate`
 * - after the script finishes, delete it or move outside of scripts folder.
 */

const fs = require('fs');
const path = require('path');
const config = hexo.config;
const http = require('http');
const https = require('https');

/**
 * Terminal colors.
 *
 * Usage:
 * console.log(`${g('I')} love ${r('Italy')}`);
 * console.log(g(`This is your name: ${name}`));
 * console.log(r(`Red `), g(`Red `), b(`Red `));
 *
 * Hint: r,g,b,w,c,m,y,k stands for Red, Green, Blue, White, Cyan, Magenta,
 * Yellow and Blac(k).
 *
 * @source https://stackoverflow.com/a/46705010
 */
const { r, g, b, w, c, m, y, k } = [
  ['r', 1], ['g', 2], ['b', 4], ['w', 7],
  ['c', 6], ['m', 5], ['y', 3], ['k', 0],
].reduce((cols, col) => ({
  ...cols,  [col[0]]: f => `\x1b[3${col[1]}m${f}\x1b[0m`
}), {});


/**
 * Replaces with multiple string pairs in one go.
 *
 * @source https://stackoverflow.com/a/16577007
 */
String.prototype.allReplace = function(obj) {
	var retStr = this;
	for (var x in obj) {
		retStr = retStr.replace(new RegExp(x, 'g'), obj[x]);
	}
	return retStr;
};

/**
 * Makes the string safe (escape) for running regex with it.
 *
 * @source https://esdiscuss.org/topic/regexp-escape#content-10
 *
 * @param thestring str The string to make it safe for regex.
 * @return str
 */
function regex_safe_string(thestring) {
	return thestring.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}

/**
 * Downloads a bunch of urls in an array and returns array to replace them
 * in content.
 *
 * @param urlList array An array holding all the image urls to be downloaded.
 * @param destDirectory str Destination directory where all images will be
 *   downloaded to.
 * @param relatedFile str (optional) But required if you want to get relative
 *   url to the images in the return array.
 * @return array
 */
async function downloadFiles(urlList, destDirectory, relatedFile='', regexSafe=true) {
	return new Promise(async (resolve) => {
		var replaceUrlList=[];
		var requestClient = http;
		for (const url of urlList) {
			var destFile = destDirectory + path.sep + path.basename(url);
			if (relatedFile) {
				var destFileReplace = path.relative(relatedFile, destFile);
				// Gets rid of the prepending '../' that gets returned by
				// path.relative() function.
				destFileReplace = destFileReplace.replace('../', '');
			} else {
				var destFileReplace = destFile;
			}
			// Deletes files from previous failed attempts (files that got
			// created but could not complete download).
			// Please note that it works most of the times but not 100% of the
			// times. So it is recommended that the process is not inturrupted
			// (e.g. with Ctrl+C).
			if (fs.existsSync(destFile)) {
				if (fs.statSync(destFile)['size'] == 0) {
					try {
						fs.unlinkSync(destFile);
					} catch(err) {
						console.error(r(err));
					}
				}
			}
			if (!fs.existsSync(destFile)) {
				try {
					var file = fs.createWriteStream(destFile);
					requestClient=(url.startsWith("https:")) ? https : http;
					var prom = await new Promise((resolve, reject) => {
						var request = requestClient.get(url, function(response) {
							response.pipe(file);
							console.log(g('-- Downloaded: '+url));
							resolve();
						});
						// Handles request failure
						request.on('error', err => {
							fs.unlink(destFile, () => reject(err));
						});
						// Handles file system failure
						file.on('error', err => {
							fs.unlink(destFile, () => reject(err));
						});
					});
					if (regexSafe==true) {
						replaceUrlList[regex_safe_string(url)] = destFileReplace;
					} else {
						replaceUrlList[url] = destFileReplace;
					}
					//replaceUrlList[url] = '{% asset_img "'+path.basename(destFile)+'" "some title" %}';
				} catch (e) {
					// Push existing url, since we failed to download it
					if (regexSafe==true) {
						replaceUrlList[regex_safe_string(url)] = url;
					} else {
						replaceUrlList[url] = url;
					}
					console.error(r('[Error]: ') + destFile + '\n' + e.message);
				}
			} else {
				if (regexSafe==true) {
					replaceUrlList[regex_safe_string(url)] = destFileReplace;
				} else {
					replaceUrlList[url] = destFileReplace;
				}
				//replaceUrlList[url] = '{% asset_img "'+path.basename(destFile)+'" "some title" %}';
			}
		};
		resolve(replaceUrlList);
	});
}

async function process(file) {
	// look into only .md files and ignore others
	if (!fs.existsSync(file) || !file.match(/\.md/)) {
		return;
	}
	return new Promise(async (resolve) => {
		// Gets directory to put downloaded images into
		var imageDir = file.match(/(.*)\.md/)[1];
		console.log(c(`== Processing file: ${file}`));
		if (fs.existsSync(imageDir)) {
			console.log(g("-- Image directory "+imageDir+" found"));
		} else {
			fs.mkdirSync(imageDir);
			console.log(y("-- Image directory not found. Created directory "+imageDir));
		}
		try {
			var encoding = 'utf-8';
			var content = fs.readFileSync(file, encoding);
			var imageUrls = content.matchAll(/\!\[.*\]\((https?:.*.(?:jpg|gif|png)).*\)/g);
			imageUrls = Array.from(imageUrls, m => m[1]);
			if (imageUrls) {
				var replaces = await downloadFiles(imageUrls, imageDir, file);
				if (Object.keys(replaces).length > 0) {
					content = await content.allReplace(replaces);
					fs.writeFileSync(file, content, encoding);
					console.log(c("-- Replaced "+Object.keys(replaces).length+" image urls"));
				} else {
					console.log(g("-- No replacements were needed for image urls"));
				}
			} else {
				console.log(c("-- No image URLs found"));
			}
		} catch (e) {
			console.error(r('[Error]: ') + file + '\n' + e.message);
		}
		resolve();
	});
}

async function walk(dir) {
	if (!fs.existsSync(dir)) {
		return;
	}
	var files = fs.readdirSync(dir);
	for (const fileName of files) {

		var curPath = path.join(dir, fileName);
		var stats = fs.statSync(curPath);
		if (stats.isFile()) {
			await process(curPath);
		} else {
			await walk(curPath);
		}
	};
}

async function run(locals, render, callback) {
	// Check for the requirements
	if (!config.post_asset_folder) {
		console.error(r('[Error]: ')+'Setting `post_asset_folder: true` in `_config.yml` is only currently supported');
		return;
	}
	try {
		require.resolve("hexo-asset-link");
	} catch(e) {
		console.error(r("[Error]: ")+"hexo-asset-link package is not found. Please install with `npm i -s hexo-asset-link`");
		return;
	}

	console.log(y("WARNING! The auto image upload process has started. Do not inturrupt the process (e.g. by pressing Ctrl+C). Doing this may result in empty image files."));
	console.log("-------------------------------------------------------------");

	// Run the download and replace...
	await walk(source_dir);

	console.log("-------------------------------------------------------------");
	console.log(y("== IMPORTANT!! The replacement procedure was completed. To not run it everytime you run `hexo generate`, delete "+__filename+" or move this file elsewhere out of the scripts folder."));

	if (callback) { callback(); }
	locals.posts.map(function(post){
		//console.log(post);
	});
}

// Since we are running this from `scripts`, we would go up one directory
var public_dir = path.resolve(__dirname, '../')+path.sep+config.public_dir;
var source_dir = path.resolve(__dirname, '../')+path.sep+config.source_dir;

(async() => {
	var generator = hexo.extend.generator;

	if (generator.register.length === 1) {
		generator.register(run);
	} else {
		generator.register('autouploadimages', run);
	}
})();